# Install required libraries (run once)
!pip install osmnx geopandas folium branca pandas
Requirement already satisfied: osmnx in /usr/local/lib/python3.10/site-packages (2.0.6) Requirement already satisfied: geopandas in /usr/local/lib/python3.10/site-packages (1.1.1) Requirement already satisfied: folium in /usr/local/lib/python3.10/site-packages (0.19.4) Requirement already satisfied: branca in /usr/local/lib/python3.10/site-packages (0.8.1) Requirement already satisfied: pandas in /usr/local/lib/python3.10/site-packages (2.1.4) Requirement already satisfied: networkx>=2.5 in /usr/local/lib/python3.10/site-packages (from osmnx) (3.4.2) Requirement already satisfied: numpy>=1.22 in /usr/local/lib/python3.10/site-packages (from osmnx) (1.24.2) Requirement already satisfied: requests>=2.27 in /usr/local/lib/python3.10/site-packages (from osmnx) (2.31.0) Requirement already satisfied: shapely>=2.0 in /usr/local/lib/python3.10/site-packages (from osmnx) (2.1.2) Requirement already satisfied: pyogrio>=0.7.2 in /usr/local/lib/python3.10/site-packages (from geopandas) (0.10.0) Requirement already satisfied: packaging in /usr/local/lib/python3.10/site-packages (from geopandas) (25.0) Requirement already satisfied: pyproj>=3.5.0 in /usr/local/lib/python3.10/site-packages (from geopandas) (3.7.0) Requirement already satisfied: jinja2>=2.9 in /usr/local/lib/python3.10/site-packages (from folium) (3.1.2) Requirement already satisfied: xyzservices in /usr/local/lib/python3.10/site-packages (from folium) (2025.1.0) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.10/site-packages (from pandas) (2.8.2) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/site-packages (from pandas) (2022.7) Requirement already satisfied: tzdata>=2022.1 in /usr/local/lib/python3.10/site-packages (from pandas) (2024.2) Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/site-packages (from jinja2>=2.9->folium) (2.1.1) Requirement already satisfied: certifi in /usr/local/lib/python3.10/site-packages (from pyogrio>=0.7.2->geopandas) (2025.1.31) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0) Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/site-packages (from requests>=2.27->osmnx) (3.4.1) Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/site-packages (from requests>=2.27->osmnx) (3.4) Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/site-packages (from requests>=2.27->osmnx) (2.3.0)
#Import libraries
import os
import pandas as pd
import geopandas as gpd
import osmnx as ox
import folium
import branca.colormap as cm
#Adding chicago traffic csv data which I downloded from Chicago govt website
crashes = pd.read_csv("Traffic_Crashes_20251115.csv")
# Converting crash records to GeoDataFrame
crash_gdf = gpd.GeoDataFrame(
crashes,
geometry=gpd.points_from_xy(crashes["LONGITUDE"], crashes["LATITUDE"]),
crs="EPSG:4326"
)
crash_gdf.head()
/var/folders/tn/qthxn6q95rd17mg5gsz5ctjh0000gq/T/ipykernel_9571/3258216460.py:3: DtypeWarning: Columns (10) have mixed types. Specify dtype option on import or set low_memory=False.
crashes = pd.read_csv("Traffic_Crashes_20251115.csv")
| CRASH_RECORD_ID | CRASH_DATE_EST_I | CRASH_DATE | POSTED_SPEED_LIMIT | TRAFFIC_CONTROL_DEVICE | DEVICE_CONDITION | WEATHER_CONDITION | LIGHTING_CONDITION | FIRST_CRASH_TYPE | TRAFFICWAY_TYPE | ... | INJURIES_REPORTED_NOT_EVIDENT | INJURIES_NO_INDICATION | INJURIES_UNKNOWN | CRASH_HOUR | CRASH_DAY_OF_WEEK | CRASH_MONTH | LATITUDE | LONGITUDE | LOCATION | geometry | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 04f52e7dd6cba4e59b164bc1b0a72914e02dcb72c1e8cb... | NaN | 11/16/2025 02:53:00 AM | 25 | NO CONTROLS | NO CONTROLS | CLEAR | DARKNESS, LIGHTED ROAD | PARKED MOTOR VEHICLE | ALLEY | ... | 0.0 | 1.0 | 0.0 | 2 | 1 | 11 | 41.843939 | -87.728338 | POINT (-87.728337854378 41.843938811471) | POINT (-87.72834 41.84394) |
| 1 | 74a5da421beb4d046a7c1bf5a9264c83661d9b758846b3... | NaN | 11/16/2025 02:45:00 AM | 30 | NO CONTROLS | NO CONTROLS | CLEAR | DARKNESS, LIGHTED ROAD | SIDESWIPE SAME DIRECTION | DIVIDED - W/MEDIAN (NOT RAISED) | ... | 0.0 | 2.0 | 0.0 | 2 | 1 | 11 | 41.903590 | -87.659213 | POINT (-87.659213209195 41.903590314092) | POINT (-87.65921 41.90359) |
| 2 | 469e9b3f61e06160cedc3fab2f1c6b0474d5740081b9ad... | NaN | 11/16/2025 02:30:00 AM | 30 | UNKNOWN | UNKNOWN | CLEAR | DARKNESS | SIDESWIPE SAME DIRECTION | UNKNOWN INTERSECTION TYPE | ... | 0.0 | 2.0 | 0.0 | 2 | 1 | 11 | 41.895049 | -87.745956 | POINT (-87.745955516269 41.895049037964) | POINT (-87.74596 41.89505) |
| 3 | 9e69f2b053b40e3044d61cf2edb96e8a4a187d27cdde17... | NaN | 11/16/2025 01:30:00 AM | 30 | NO CONTROLS | NO CONTROLS | CLEAR | DARKNESS, LIGHTED ROAD | PARKED MOTOR VEHICLE | ONE-WAY | ... | 0.0 | 1.0 | 0.0 | 1 | 1 | 11 | 41.657918 | -87.596296 | POINT (-87.596296309543 41.657918181213) | POINT (-87.5963 41.65792) |
| 4 | e736b23d346f4b4c5c96f35388f58a8c0cda11d0a955a3... | NaN | 11/16/2025 01:30:00 AM | 30 | TRAFFIC SIGNAL | FUNCTIONING PROPERLY | CLEAR | DARKNESS, LIGHTED ROAD | TURNING | FOUR WAY | ... | 0.0 | 2.0 | 0.0 | 1 | 1 | 11 | 41.884352 | -87.656788 | POINT (-87.656787588102 41.884351914473) | POINT (-87.65679 41.88435) |
5 rows × 49 columns
# Drop rows with missing or invalid coordinates
crashes = crashes.dropna(subset=["LATITUDE", "LONGITUDE"])
# Remove rows where lat/long are 0 or non-numeric
crashes = crashes[
(crashes["LATITUDE"] != 0) &
(crashes["LONGITUDE"] != 0)
]
crashes = crashes[crashes["CRASH_DATE"].str.startswith("2024")]
# Convert to GeoDataFrame ( datum is crucial for my next steps )
crash_gdf = gpd.GeoDataFrame(
crashes,
geometry=gpd.points_from_xy(crashes["LONGITUDE"], crashes["LATITUDE"]),
crs="EPSG:4326"
)
#lets see the colums so that I can see the colum name clearly.
print(crashes.columns)
Index(['CRASH_RECORD_ID', 'CRASH_DATE_EST_I', 'CRASH_DATE',
'POSTED_SPEED_LIMIT', 'TRAFFIC_CONTROL_DEVICE', 'DEVICE_CONDITION',
'WEATHER_CONDITION', 'LIGHTING_CONDITION', 'FIRST_CRASH_TYPE',
'TRAFFICWAY_TYPE', 'LANE_CNT', 'ALIGNMENT', 'ROADWAY_SURFACE_COND',
'ROAD_DEFECT', 'REPORT_TYPE', 'CRASH_TYPE', 'INTERSECTION_RELATED_I',
'NOT_RIGHT_OF_WAY_I', 'HIT_AND_RUN_I', 'DAMAGE', 'DATE_POLICE_NOTIFIED',
'PRIM_CONTRIBUTORY_CAUSE', 'SEC_CONTRIBUTORY_CAUSE', 'STREET_NO',
'STREET_DIRECTION', 'STREET_NAME', 'BEAT_OF_OCCURRENCE',
'PHOTOS_TAKEN_I', 'STATEMENTS_TAKEN_I', 'DOORING_I', 'WORK_ZONE_I',
'WORK_ZONE_TYPE', 'WORKERS_PRESENT_I', 'NUM_UNITS',
'MOST_SEVERE_INJURY', 'INJURIES_TOTAL', 'INJURIES_FATAL',
'INJURIES_INCAPACITATING', 'INJURIES_NON_INCAPACITATING',
'INJURIES_REPORTED_NOT_EVIDENT', 'INJURIES_NO_INDICATION',
'INJURIES_UNKNOWN', 'CRASH_HOUR', 'CRASH_DAY_OF_WEEK', 'CRASH_MONTH',
'LATITUDE', 'LONGITUDE', 'LOCATION'],
dtype='object')
crashes = pd.read_csv("Traffic_Crashes_20251115.csv", low_memory=False)
#Lets do Download Chicago street network
import osmnx as ox
# Lets add the drivable street network for Chicago
G = ox.graph_from_place("Chicago, Illinois, USA", network_type="drive")
edges = ox.graph_to_gdfs(G, nodes=False, edges=True)
edges.head()
| osmid | highway | oneway | reversed | length | geometry | lanes | maxspeed | name | ref | bridge | access | tunnel | width | junction | area | |||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| u | v | key | ||||||||||||||||
| 702090 | 261263104 | 0 | 24536463 | motorway_link | True | False | 323.562424 | LINESTRING (-87.81782 41.98343, -87.81813 41.9... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 1223297118 | 0 | [1000818941, 8220086] | motorway | True | False | 713.144140 | LINESTRING (-87.81782 41.98343, -87.81879 41.9... | 4 | 55 mph | Kennedy Expressway | I 90 | NaN | NaN | NaN | NaN | NaN | NaN | |
| 25779161 | 739968328 | 0 | 4378009 | motorway | True | False | 376.541227 | LINESTRING (-87.661 41.90109, -87.66088 41.900... | 4 | 45 mph | Kennedy Expressway | I 90;I 94 | NaN | NaN | NaN | NaN | NaN | NaN |
| 25779173 | 709393011 | 0 | [24229922, 56716748, 56716749] | motorway_link | True | False | 676.727456 | LINESTRING (-87.65698 41.89495, -87.65687 41.8... | 2 | NaN | NaN | NaN | yes | NaN | NaN | NaN | NaN | NaN |
| 25779174 | 0 | 906195924 | motorway | True | False | 110.038748 | LINESTRING (-87.65698 41.89495, -87.65651 41.8... | 4 | 45 mph | Kennedy Expressway | I 90;I 94 | NaN | NaN | NaN | NaN | NaN | NaN |
pip install shapely==1.8.5.post1
Collecting shapely==1.8.5.post1
Using cached Shapely-1.8.5.post1-cp310-cp310-macosx_10_9_x86_64.whl.metadata (43 kB)
Using cached Shapely-1.8.5.post1-cp310-cp310-macosx_10_9_x86_64.whl (1.2 MB)
Installing collected packages: shapely
Attempting uninstall: shapely
Found existing installation: shapely 2.1.2
Uninstalling shapely-2.1.2:
Successfully uninstalled shapely-2.1.2
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
geopandas 1.1.1 requires shapely>=2.0.0, but you have shapely 1.8.5.post1 which is incompatible.
osmnx 2.0.6 requires shapely>=2.0, but you have shapely 1.8.5.post1 which is incompatible.
prettymaps 1.0.0 requires osmnx==1.2.2, but you have osmnx 2.0.6 which is incompatible.
Successfully installed shapely-1.8.5.post1
Note: you may need to restart the kernel to use updated packages.
!pip install --upgrade geopandas osmnx shapely
Requirement already satisfied: geopandas in /usr/local/lib/python3.10/site-packages (1.1.1)
Requirement already satisfied: osmnx in /usr/local/lib/python3.10/site-packages (2.0.6)
Requirement already satisfied: shapely in /usr/local/lib/python3.10/site-packages (1.8.5.post1)
Collecting shapely
Using cached shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl.metadata (6.8 kB)
Requirement already satisfied: numpy>=1.24 in /usr/local/lib/python3.10/site-packages (from geopandas) (1.24.2)
Requirement already satisfied: pyogrio>=0.7.2 in /usr/local/lib/python3.10/site-packages (from geopandas) (0.10.0)
Requirement already satisfied: packaging in /usr/local/lib/python3.10/site-packages (from geopandas) (25.0)
Requirement already satisfied: pandas>=2.0.0 in /usr/local/lib/python3.10/site-packages (from geopandas) (2.1.4)
Requirement already satisfied: pyproj>=3.5.0 in /usr/local/lib/python3.10/site-packages (from geopandas) (3.7.0)
Requirement already satisfied: networkx>=2.5 in /usr/local/lib/python3.10/site-packages (from osmnx) (3.4.2)
Requirement already satisfied: requests>=2.27 in /usr/local/lib/python3.10/site-packages (from osmnx) (2.31.0)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.10/site-packages (from pandas>=2.0.0->geopandas) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/site-packages (from pandas>=2.0.0->geopandas) (2022.7)
Requirement already satisfied: tzdata>=2022.1 in /usr/local/lib/python3.10/site-packages (from pandas>=2.0.0->geopandas) (2024.2)
Requirement already satisfied: certifi in /usr/local/lib/python3.10/site-packages (from pyogrio>=0.7.2->geopandas) (2025.1.31)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/site-packages (from python-dateutil>=2.8.2->pandas>=2.0.0->geopandas) (1.17.0)
Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/site-packages (from requests>=2.27->osmnx) (3.4.1)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/site-packages (from requests>=2.27->osmnx) (3.4)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/site-packages (from requests>=2.27->osmnx) (2.3.0)
Using cached shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl (1.8 MB)
Installing collected packages: shapely
Attempting uninstall: shapely
Found existing installation: Shapely 1.8.5.post1
Uninstalling Shapely-1.8.5.post1:
Successfully uninstalled Shapely-1.8.5.post1
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
prettymaps 1.0.0 requires osmnx==1.2.2, but you have osmnx 2.0.6 which is incompatible.
prettymaps 1.0.0 requires Shapely<2.0,>=1.8, but you have shapely 2.1.2 which is incompatible.
Successfully installed shapely-2.1.2
import pandas as pd
import geopandas as gpd
import osmnx as ox
import folium
import branca.colormap as cm
from collections import Counter
# Adding and cleaning crash data
crashes = pd.read_csv("Traffic_Crashes_20251115.csv", low_memory=False)
crashes = crashes.dropna(subset=["LATITUDE", "LONGITUDE"])
crashes = crashes[
(crashes["LATITUDE"].between(41.6, 42.1)) &
(crashes["LONGITUDE"].between(-88.0, -87.5))
]
crash_gdf = gpd.GeoDataFrame(
crashes,
geometry=gpd.points_from_xy(crashes["LONGITUDE"], crashes["LATITUDE"]),
crs="EPSG:4326"
)
# loading Chicago road network and clip to central area
G = ox.graph_from_place("Chicago, Illinois, USA", network_type="drive")
edges = ox.graph_to_gdfs(G, nodes=False, edges=True)
# Cliping manually to central Chicago
edges = edges.cx[-87.75:-87.60, 41.80:41.95]
# Reprojecting and match crashes to nearest edges
crash_gdf = crash_gdf.to_crs(edges.crs)
edges = edges.to_crs(edges.crs)
x = crash_gdf.geometry.x.values
y = crash_gdf.geometry.y.values
nearest_edges = ox.distance.nearest_edges(G, x, y)
edge_counts = Counter(nearest_edges)
edges["crash_count"] = edges.index.map(lambda idx: edge_counts.get(idx, 0))
# Filtering edges with crashes otherwise big data will impact my speed and taking lot of time to complete one run
edges = edges[edges["crash_count"] > 0]
print("Edges shape:", edges.shape)
print("Crash count column exists:", "crash_count" in edges.columns)
print("Crash count summary:\n", edges["crash_count"].describe() if "crash_count" in edges.columns else "No crash_count")
Edges shape: (22156, 17) Crash count column exists: True Crash count summary: count 22156.000000 mean 21.070139 std 35.000014 min 1.000000 25% 3.000000 50% 9.000000 75% 23.000000 max 1037.000000 Name: crash_count, dtype: float64
#lets check the length of accident data
len(edges)
edges.plot()
<AxesSubplot: >
# After matching crashes to edges and calculating crash_count
edges["crash_density"] = edges["crash_count"] # or any normalized field
# Lets do save to GeoJSON
edges.to_file("chicago_crash_roads.geojson", driver="GeoJSON")
gdf = gpd.read_file("chicago_crash_roads.geojson")
Skipping field highway: unsupported OGR type: 5 Skipping field lanes: unsupported OGR type: 5 Skipping field maxspeed: unsupported OGR type: 5 Skipping field name: unsupported OGR type: 5 Skipping field bridge: unsupported OGR type: 5 Skipping field tunnel: unsupported OGR type: 5
# lets see the crash density column for finding better scale
print("Minimum accidents:", gdf["crash_density"].min())
print("Maximum accidents:", gdf["crash_density"].max())
# Or lets get a quick summary
print(gdf["crash_density"].describe())
Minimum accidents: 1 Maximum accidents: 1037 count 22156.000000 mean 21.070139 std 35.000014 min 1.000000 25% 3.000000 50% 9.000000 75% 23.000000 max 1037.000000 Name: crash_density, dtype: float64
# Now I here min accident is 1 and max is 1037 so I am going to keep 400 low and 1037 for high in hotsopt
# loading and clean GeoJSON
gdf = gpd.read_file("chicago_crash_roads.geojson")
gdf = gdf.drop(columns=["highway", "lanes", "maxspeed", "name", "bridge", "tunnel"], errors="ignore")
gdf["crash_density"] = gdf["crash_density"].fillna(0)
# lets clip to central Chicago
gdf = gdf.cx[-87.75:-87.60, 41.80:41.95]
# Defining colormap for roads (green → red, 10 steps, scale 100–1037)
road_colormap = cm.LinearColormap(
colors=list(reversed(cm.linear.RdYlGn_11.colors)),
vmin=100,
vmax=1037
).to_step(10)
# defineing colormap for hotspots (orange → dark red, ≥400 accidents)
hotspot_colormap = cm.LinearColormap(
colors=["yellow", "red", "darkred"],
vmin=400,
vmax=1037
)
# center map Cartography
minx, miny, maxx, maxy = gdf.total_bounds
center = [(miny + maxy) / 2, (minx + maxx) / 2]
m = folium.Map(location=center, zoom_start=13, tiles="CartoDB positron")
# road segments styled by crash density
folium.GeoJson(
gdf,
style_function=lambda feature: {
"color": road_colormap(feature["properties"]["crash_density"]),
"weight": 2 + (feature["properties"]["crash_density"] / 150),
"opacity": 0.9
}
).add_to(m)
# hotspot markers (≥400 accidents)
hotspots = gdf[gdf["crash_density"] >= 400]
for _, row in hotspots.iterrows():
centroid = row.geometry.centroid
folium.CircleMarker(
location=[centroid.y, centroid.x],
radius=6,
color=hotspot_colormap(row["crash_density"]),
fill=True,
fill_color=hotspot_colormap(row["crash_density"]),
fill_opacity=0.8,
popup=f"Hotspot: {row['crash_density']} accidents"
).add_to(m)
# legends (both on right side)
road_colormap.caption = "Road crash density (100–1037)"
road_colormap.add_to(m)
hotspot_colormap.caption = "Hotspot density (yellow → dark red)"
hotspot_colormap.add_to(m)
# heading
title_html = """
<h1 align="center" style="font-size:28px; color:#2C3E50;">
<b>Chicago Traffic Accident Hotspots</b>
</h1>
"""
m.get_root().html.add_child(folium.Element(title_html))
# lets make it fit bounds and save
m.fit_bounds([[miny, minx], [maxy, maxx]])
m.save("chicago_accidents_hotspots_2025.html")
m
Skipping field highway: unsupported OGR type: 5 Skipping field lanes: unsupported OGR type: 5 Skipping field maxspeed: unsupported OGR type: 5 Skipping field name: unsupported OGR type: 5 Skipping field bridge: unsupported OGR type: 5 Skipping field tunnel: unsupported OGR type: 5